// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Utf8Json;
using Utf8Json.Formatters;

namespace SnapObjects.Data
{
    internal class UTF8JNodeReader : INodeReader
    {
        private object _value;
        private JsonReader _reader;
        private JsonToken _jsonToken;

        private readonly Stack<byte> _levels;

        private UTF8JNodeReader(string raw)
        {
            _levels = new Stack<byte>();

            _reader = new JsonReader(Encoding.UTF8.GetBytes(raw));

            this.InitNode();
        }

        public bool IsEnd { get; private set; }
        public string Name { get; private set; }
        public string CurrentNodeName { get; private set; }
        public DataNodeType NodeType { get; private set; }
        public DataNodeType CurrentNodeType { get; private set; }

        public IValueReader ValueReader
        {
            get
            {
                this.CheckEnd();

                if (this.CurrentNodeType != DataNodeType.Value)
                {
                    throw new JsonParsingException($"Unexpected token '{this.Name}'.");
                }

                return new UTF8ValueReader(this.CurrentNodeName, _value, _jsonToken);
            }
        }

        public static INodeReader Parse(string raw)
        {
            if (String.IsNullOrWhiteSpace(raw))
            {
                throw new ArgumentException(Properties.Resource.DATAREADER_NOTNULL_EXCEPTION);
            }

            return new UTF8JNodeReader(raw);
        }

        private void CheckEnd()
        {
            if (this.IsEnd)
            {
                throw new JsonParsingException("The node has been read to the end.");
            }
        }

        public bool Read()
        {
            this.CheckEnd();

            var token = _reader.GetCurrentJsonToken();

            if (token == JsonToken.None)
            {
                this.IsEnd = true;

                return false;
            }

            if ((token == JsonToken.EndObject && this.NodeType == DataNodeType.Object) ||
                (token == JsonToken.EndArray && this.NodeType == DataNodeType.Array))
            {
                _reader.ReadNext();
                _reader.ReadIsValueSeparator();

                if (_levels.Count == 0)
                {
                    this.IsEnd = true;

                    return false;
                }

                _levels.Pop();

                if (_levels.Count == 0)
                {
                    token = _reader.GetCurrentJsonToken();

                    if (token == JsonToken.BeginObject || token == JsonToken.EndArray)
                    {
                        this.NodeType = DataNodeType.Array;

                        return false;
                    }

                    this.IsEnd = true;

                    return false;
                }

                this.NodeType = _levels.Peek() == 0 ? DataNodeType.Object : DataNodeType.Array;

                return false;
            }

            if (this.NodeType == DataNodeType.Object)
            {
                this.CurrentNodeName = _reader.ReadPropertyName();

                _jsonToken = _reader.GetCurrentJsonToken();

                _value = this.GetValue();

                token = _reader.GetCurrentJsonToken();
            }
            else if (this.NodeType == DataNodeType.Array &&
                (token == JsonToken.String ||
                token == JsonToken.Number ||
                token == JsonToken.True ||
                token == JsonToken.False ||
                token == JsonToken.Null))
            {
                _jsonToken = _reader.GetCurrentJsonToken();

                _value = this.GetValue();
            }

            switch (token)
            {
                case JsonToken.BeginObject:
                {
                    this.CurrentNodeType = DataNodeType.Object;
                }
                break;
                case JsonToken.BeginArray:
                {
                    this.CurrentNodeType = DataNodeType.Array;
                }
                break;
                default:
                {
                    this.CurrentNodeType = DataNodeType.Value;
                }
                break;
            }

            return true;
        }

        public INodeReader ReadAsNode()
        {
            this.CheckEnd();

            var token = _reader.GetCurrentJsonToken();

            if (this.CurrentNodeType == DataNodeType.Object && token == JsonToken.BeginObject)
            {
                _levels.Push(0);

                _reader.ReadIsBeginObject();

                this.Name = this.CurrentNodeName;

                this.NodeType = this.CurrentNodeType;

                return this;
            }
            else if (this.CurrentNodeType == DataNodeType.Array && token == JsonToken.BeginArray)
            {
                _levels.Push(1);

                _reader.ReadIsBeginArray();

                this.Name = this.CurrentNodeName;

                this.NodeType = this.CurrentNodeType;

                return this;
            }

            throw new JsonParsingException($"Unexpected token '{_value}'.");
        }

        public IValueReader ReadAsRaw()
        {
            this.CheckEnd();

            var name = this.CurrentNodeName;
            var token = _reader.GetCurrentJsonToken();

            if (token == JsonToken.BeginArray || token == JsonToken.BeginObject)
            {
                var block = _reader.ReadNextBlockSegment();

                var value = Encoding.UTF8.GetString(block.ToArray());

                _reader.ReadIsValueSeparator();

                this.Read();

                token = _reader.GetCurrentJsonToken();

                if (token == JsonToken.EndObject || token == JsonToken.EndArray)
                {
                    this.IsEnd = true;
                }

                return new UTF8ValueReader(name, value, JsonToken.String);
            }
            else
            {
                var value = _value;
                var jsonToken = _jsonToken;

                this.Read();

                return new UTF8ValueReader(name, value, jsonToken);
            }
        }

        public void SkipCurrentNode()
        {
            this.CheckEnd();

            _reader.ReadNextBlock();
        }

        private void InitNode()
        {
            var token = _reader.GetCurrentJsonToken();

            if (token == JsonToken.None)
            {
                this.IsEnd = true;

                return;
            }

            if (token == JsonToken.BeginObject)
            {
                this.NodeType = DataNodeType.Object;
            }
            else if (token == JsonToken.BeginArray)
            {
                this.NodeType = DataNodeType.Array;
            }
            else
            {
                throw new JsonParsingException($"Unexpected token '{(char)_reader.GetBufferUnsafe()[0]}'.");
            }

            this.IsEnd = false;

            _reader.ReadNext();
        }

        private object GetValue()
        {
            object value = null;

            switch (_reader.GetCurrentJsonToken())
            {
                case JsonToken.String:
                {
                    value = _reader.ReadString();
                    _reader.ReadIsValueSeparator();
                }
                break;
                case JsonToken.Number:
                {
                    var offset = _reader.GetCurrentOffsetUnsafe();

                    var doubleValue = _reader.ReadDouble();

                    var readDoubleOffset = _reader.GetCurrentOffsetUnsafe();

                    _reader.AdvanceOffset(offset - readDoubleOffset);

                    var numSegment = _reader.ReadNumberSegment();

                    var readNumSegmentOffset = _reader.GetCurrentOffsetUnsafe();

                    var diff = readDoubleOffset - readNumSegmentOffset;

                    if (diff != 0)
                    {
                        _reader.AdvanceOffset(diff);

                        value = doubleValue;
                    }
                    else
                    {
                        var segment = new byte[numSegment.Count];

                        Array.Copy(numSegment.Array, numSegment.Offset, segment, 0, numSegment.Count);

                        value = Encoding.UTF8.GetString(segment);

                    }

                    _reader.ReadIsValueSeparator();
                }
                break;
                case JsonToken.True:
                case JsonToken.False:
                {
                    value = _reader.ReadBoolean();
                    _reader.ReadIsValueSeparator();
                }
                break;
                case JsonToken.Null:
                {
                    _reader.ReadIsNull();
                    _reader.ReadIsValueSeparator();
                }
                break;
            }

            return value;
        }
    }
}
